#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <netinet/sctp.h>
#include <iostream>
#include <stdexcept>
#include <pthread.h>
#include <signal.h>

#include "defines.h"
#include "sctp_one_to_many_connection.h"

#define PORT 6789
#define MAX_USERNAME_LENGTH 15

SctpOneToManyConnection connection;
Address serverAddress;

// Reads input from the user, via the console, and sends the input to the
// server.
void* handleUserInput(void* dummy)
{
    bool hasReceivedQuitCommand = false;
    ssize_t sentBytes;
    std::string readLine;
    struct ChatMessage message, dummyBuffer;
    SctpReceiveResult result;
    sctp_assoc_t assocId;

    // Clear message buffer
    memset(&message, '\0', sizeof(message));

    // Read user name from the user
    std::cout << "Enter username: " << std::endl;
    getline(std::cin, readLine);

    // Copy user name to buffer
    strncpy(message.username, readLine.c_str(), sizeof(message.username));
    message.username[MAX_USERNAME_SIZE - 1] = '\0';


    // Ask the server for the message buffer contents:
    strncpy(message.message, "/getbuffer", sizeof(message.message));
    sentBytes = connection.sendToAddress(
            &message, sizeof(message), serverAddress);

    if (sentBytes < 0)
        throw std::runtime_error("Could not connect to server");

    // Since the /getbuffer command is the first message, do a single receive
    // to handle the association event, so that the other thread can see
    // that a notification has been created:
    result = connection.receive(&dummyBuffer, sizeof(dummyBuffer));

    if (result.getMessageSize() < 0)
        throw std::runtime_error("Could not handle first association event");

    // Get the association id for the new association to the server:
    assocId = connection.getAssociationId(serverAddress);
    std::cout << "Connected to the server, the association id is: "
              << assocId << std::endl;

    while(!hasReceivedQuitCommand)
    {
        // Read a line from the console
        getline(std::cin, readLine);

        if (readLine == "/quit")
            hasReceivedQuitCommand = true;

        // Copy the user input to the message buffer:
        strncpy(message.message, readLine.c_str(), sizeof(message.message));

        // Make sure the string is null terminated:
        message.message[MSG_SIZE-1] = '\0';

        // If no association has been established yet, do a single receive
        // to handle the association event:
        if (connection.getAssociationCount() == 0)
        {
            // Send the message to the server:
            sentBytes = connection.sendToAddress(
                    &message, sizeof(message), serverAddress);

            // Do a single receive to handle the association event, and get the
            // new association id:
            connection.receive(&dummyBuffer, sizeof(dummyBuffer));
            assocId = connection.getAssociationId(serverAddress);
        }
        else
        {
            sentBytes = connection.send(&message, sizeof(message), assocId);
        }

        // Check for errors:
        if (sentBytes < 0)
        {
            std::cout << "Error while sending: " << errno << std::endl;
            perror("client send");
        }
    }

    pthread_exit(NULL);
}

// Reads messages from the server. If they are commands they are handled
// and if they are ordinary text messages they are printed to the console.
void* handleServerMessages(void* dummy)
{
    struct ChatMessage message;
    SctpReceiveResult result;

    while(1)
    {
        // If there are no active associations on the connection, don't try
        // to receive a message.
        if (connection.getAssociationCount() == 0)
        {
            // To avoid frantic looping and polling of the association count,
            // sleep one second if there are no active associations.
            // The slight latency should be better than an unnecessarily high
            // CPU usage.
            sleep(1);

            // Go to next iteration, no receive should be done.
            continue;
        }

        // Receive a message from the server:
        result = connection.receiveMessage(&message, sizeof(message));

        // Check for error, and print error message:
        if (result.getMessageSize() < 0)
        {
            int errorNumber = result.getErrorNumber();

            if (errorNumber == ENOTCONN)
                std::cout << "Disconnected from server" << std::endl;
            else
                std::cout << "Receive error: " << errorNumber << std::endl;

            // Go to next iteration, the message should not be printed.
            continue;
        }

        std::cout << message.timestamp << " "
                  << message.username << ": "
                  << message.message << std::endl;
    }

    pthread_exit(NULL);
}

// Chat client. Spawns two threads, one for reading user input and sending
// it to the server and one for reading server messages and printing them
// to the console.
int main(int argc, char* argv[])
{
    std::vector<Address> addresses, boundAddresses;
    pthread_t receiveThread, sendThread;
    pthread_attr_t detachedAttr;

    // Check command line argument count
    if (argc < 3)
    {
        std::cerr << "Usage: " << argv[0] << " <server address> <port>"
                  << " <client address 1> ... <client address n>"  << std::endl;
        exit(-1);
    }

    // Create address instance from command line arguments
    serverAddress = Address(argv[1], atoi(argv[2]));

    // Create an instance of Address for each IP given by the user.
    // Use the port 0. An ephemeral port will be assigned by the system.
    if (argc > 3)
    {
        // Create a vector of all the IP addresses given as parameters:
        for (int i = 3; i < argc; i++)
            addresses.push_back(Address(argv[i], 0));

        connection.bind(addresses);
    }
    // If the user did not explicitly specify which addresses to use,
    // bind all available addresses:
    else
    {
        connection.bindToAll(0);
    }

    // Output which addresses are currently bound:
    std::cout << "Your connection will use the following addresses:"
              << std::endl;

    // This call might seem unnecessary if explicit IP:s have been specified
    // to be bound, but it makes sure that those IP:s where actually bound.
    connection.getBoundAddresses(boundAddresses);

    // Print IP address and host name for each bound address:
    int boundCount = boundAddresses.size();
    for (int i = 0; i < boundCount; i++)
    {
        std::cout << boundAddresses[i].getDottedDecimalRepresentation()
                  << " / " << boundAddresses[i].getHostName()
                  << ", port: " << boundAddresses[i].getPortNumber()
                  << std::endl;
    }

    // Initialize POSIX threads
    pthread_attr_init(&detachedAttr);
    pthread_attr_setdetachstate(&detachedAttr, PTHREAD_CREATE_DETACHED);

    // Create thread that reads user input. Create it as joinable (default),
    // since the user is the one who decides when the program should terminate.
    pthread_create(&sendThread, NULL, &handleUserInput, NULL);

    // Create thread that receives messages from the server. Create it as
    // detached, since this thread should run until the program terminates
    // and will not have any useful return value or such.
    pthread_create(&receiveThread, &detachedAttr, &handleServerMessages, NULL);

    // Wait for user input thread to exit:
    pthread_join(sendThread, NULL);

    // Clean up
    pthread_attr_destroy(&detachedAttr);
    connection.close();

    return 0;
}
